package com.neuronbit.businessflow.flow.source;

import com.neuronbit.businessflow.flow.clazz.ClassInfo;
import com.neuronbit.businessflow.flow.clazz.MethodInfo;
import com.neuronbit.businessflow.flow.parser.FlowNode;
import com.neuronbit.businessflow.flow.parser.Output;
import com.neuronbit.businessflow.flow.project.Project;
import com.neuronbit.businessflow.flow.source.util.Imports;
import com.neuronbit.businessflow.flow.source.util.SourceContext;
import com.neuronbit.businessflow.intellij.model.ExceptionHandler;
import com.neuronbit.businessflow.intellij.model.*;
import com.neuronbit.businessflow.runtime.Flow;
import com.neuronbit.businessflow.runtime.*;
import com.neuronbit.javaparser.ast.CompilationUnit;
import com.neuronbit.javaparser.ast.Modifier;
import com.neuronbit.javaparser.ast.NodeList;
import com.neuronbit.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.neuronbit.javaparser.ast.body.ConstructorDeclaration;
import com.neuronbit.javaparser.ast.body.MethodDeclaration;
import com.neuronbit.javaparser.ast.body.Parameter;
import com.neuronbit.javaparser.ast.expr.MethodCallExpr;
import com.neuronbit.javaparser.ast.expr.NameExpr;
import com.neuronbit.javaparser.ast.nodeTypes.NodeWithStatements;
import com.neuronbit.javaparser.ast.stmt.BlockStmt;
import com.neuronbit.javaparser.ast.stmt.CatchClause;
import com.neuronbit.javaparser.ast.stmt.IfStmt;
import com.neuronbit.javaparser.ast.stmt.TryStmt;
import com.neuronbit.javaparser.ast.type.ClassOrInterfaceType;
import com.neuronbit.javaparser.resolution.types.ResolvedType;
import lombok.AccessLevel;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;

import java.util.*;

import static java.lang.String.format;
import static org.apache.commons.lang3.StringUtils.capitalize;

/**
 * The type Abstract node source generator.
 */
public abstract class StepJavaSource implements JavaSource {
    @Getter(AccessLevel.PROTECTED)
    private final Node node;
    @Getter(AccessLevel.PROTECTED)
    private final ContextJavaSource contextSource;
    @Getter(AccessLevel.PROTECTED)
    private final Project project;
    @Getter(AccessLevel.PROTECTED)
    private final FlowJavaSource flowNodeSource;
    @Getter(AccessLevel.PROTECTED)
    private final FlowNode flow;
    @Getter(AccessLevel.PROTECTED)
    private ClassOrInterfaceDeclaration stepClass;

    private final String packageName;
    private final Imports imports = new Imports();
    private CompilationUnit cu;

    /**
     * Instantiates a new Abstract node source generator.
     *
     * @param node          the node
     * @param sourceContext the source context
     */
    public StepJavaSource(Node node, SourceContext sourceContext) {
        this.node = node;
        this.contextSource = sourceContext.getContextSource();
        this.packageName = sourceContext.getPackageName();
        this.flowNodeSource = sourceContext.getFlowNodeSource();
        this.project = sourceContext.getProject();
        this.flow = sourceContext.getFlowNode();
        init();
    }

    /**
     * Add import.
     *
     * @param type the type
     */
    protected void addImport(Type type) {
        imports.add(type);
    }

    /**
     * Add import.
     *
     * @param type the type
     */
    protected void addImport(ResolvedType type) {
        imports.add(type);
    }

    @Override
    public CompilationUnit getCompilationUnit() {
        return cu;
    }

    @Override
    public String getFileName() {
        return stepClass.getNameAsString();
    }

    /**
     * Add extended type.
     */
    protected void addExtendedType() {
        cu.addImport(Step.class);
        stepClass.addExtendedType(format("Step<%s>", contextSource.getSimpleName()));
    }

    /**
     * Add constructor.
     */
    protected void addConstructor() {
        String name = node.getName();
        String id = node.getId();
        String description = node.getDescription();

        ConstructorDeclaration constructor = stepClass.addConstructor(Modifier.Keyword.PUBLIC);
        constructor.setBody(new BlockStmt());
        cu.addImport(StepType.class);
        constructor.getBody().addStatement(format("super(\"%s\", \"%s\", \"%s\", StepType.%s);", id, name, description, getStepType().name()));
    }

    /**
     * Gets step type.
     *
     * @return the step type
     */
    protected abstract StepType getStepType();

    public void generate() {
        addExtendedType();
        addConstructor();

        BlockStmt callMethodBody = new BlockStmt();
        callMethodBody.addStatement("this.start(context, flow);");

        BlockStmt stepCallStmt = new BlockStmt();
        writeStepCallStmt(stepCallStmt);
        MethodDeclaration runMethod = stepClass.addMethod("run", Modifier.Keyword.PUBLIC);
        runMethod.setType(void.class);
        runMethod.addParameter(contextSource.getSimpleName(), "context");
        runMethod.addParameter("Flow", "flow");
        runMethod.addThrownException(Exception.class);
        runMethod.setBody(stepCallStmt);

        BlockStmt callRunMethodStmt = new BlockStmt();
        IfStmt ifStmt = new IfStmt();
        ifStmt.setCondition(new MethodCallExpr("context.shouldRun", new NameExpr("this")));

        BlockStmt thenStmt = new BlockStmt();
        thenStmt.addStatement("run(context, flow);");
        writeFlowOutput(thenStmt);
        thenStmt.addStatement("this.end(context, flow);");
        ifStmt.setThenStmt(thenStmt);

        BlockStmt elseStmt = new BlockStmt();
        elseStmt.addStatement("this.skipped(context, flow);");
        ifStmt.setElseStmt(elseStmt);

        callRunMethodStmt.addStatement(ifStmt);

        afterStepCallStmt(handleRuntimeException(callRunMethodStmt), callMethodBody);

        MethodDeclaration callMethod = stepClass.addMethod("call", Modifier.Keyword.PUBLIC);
        callMethod.setType(void.class);
        callMethod.addParameter(contextSource.getSimpleName(), "context");
        callMethod.addParameter("Flow", "flow");
        callMethod.addThrownException(Exception.class);
        callMethod.setBody(callMethodBody);

        imports.install(cu);
    }

    private BlockStmt handleRuntimeException(BlockStmt callRunMethodStmt) {
        BlockStmt body = new BlockStmt();
        BlockStmt tryBody = new BlockStmt();
        callRunMethodStmt.getStatements().forEach(tryBody::addStatement);

        BlockStmt catchBody = new BlockStmt();
        catchBody.addStatement("this.pause(context, flow);");
        catchBody.addStatement("return;");

        ClassOrInterfaceType exceptionType = new ClassOrInterfaceType("PauseFlowExecutionException");
        addImport(SimpleType.of("com.neuronbit.businessflow.runtime.exception.PauseFlowExecutionException"));
        CatchClause catchClause = new CatchClause(new Parameter(exceptionType, "ignore"), catchBody);

        body.addStatement(new TryStmt(tryBody, NodeList.nodeList(catchClause), null));
        return body;
    }

    private void init() {
        String name = node.getName();
        cu = new CompilationUnit(packageName);
        cu.addImport(Flow.class);
        cu.addImport(NullSafe.class);
        cu.addImport(StepBeanProvider.class);
        cu.addImport(contextSource.getFullyQualifiedName());
        stepClass = cu.addClass(capitalize(capitalize(name) + "Step"));
        stepClass.setFinal(true);
        stepClass.setModifiers(Modifier.Keyword.PUBLIC);
    }

    /**
     * Write step call stmt.
     *
     * @param stepCallStmt the step call stmt
     */
    protected abstract void writeStepCallStmt(BlockStmt stepCallStmt);

    /**
     * After step call stmt.
     *
     * @param stepCallStmt the step call stmt
     * @param methodBody   the method body
     */
    protected void afterStepCallStmt(BlockStmt stepCallStmt, BlockStmt methodBody) {
        processExceptionHandler(stepCallStmt, methodBody);

        for (Node successor : this.flow.successors(getNode())) {
            //join should be processed by fork node
            if (!(successor instanceof Join)) {
                handlerSuccessor(successor, methodBody);
            }
        }
    }

    protected void processExceptionHandler(BlockStmt stepCallStmt, BlockStmt methodBody) {
        List<MethodInfo> exceptionHandlerMethods = getExceptionHandlerMethods();
        if (exceptionHandlerMethods.isEmpty()) {
            //No Exception Handler
            stepCallStmt.getStatements().forEach(methodBody::addStatement);
        } else {
            NodeList<CatchClause> catchClauseList = new NodeList();
            for (MethodInfo exceptionHandlerMethod : exceptionHandlerMethods) {
                // Create a block of statements for the catch clause
                BlockStmt catchBody = new BlockStmt();
                catchBody.addStatement(format("boolean shouldStop = flow.getStepBeanProvider().getBean(%s.class).%s(%s);",
                        exceptionHandlerMethod.getParent().getQualifiedName(),
                        exceptionHandlerMethod.getName(), "e"));

                catchBody.addStatement("if(shouldStop) {return;}");

                // Create the exception type
                String exceptionTypeName = exceptionHandlerMethod.getParameterTypes()[0].getQualifiedName();
                ClassOrInterfaceType exceptionType = new ClassOrInterfaceType(exceptionTypeName);
                addImport(SimpleType.of(exceptionTypeName));

                // Create the CatchClause
                catchClauseList.add(new CatchClause(new Parameter(exceptionType, "e"), catchBody));
            }

            methodBody.addStatement(new TryStmt(stepCallStmt, catchClauseList, null));
        }
    }

    private List<MethodInfo> getExceptionHandlerMethods() {
        List<ExceptionHandler> exceptionHandlers = new ArrayList<>();

        ExceptionHandler flowExceptionHandler = getFlow().getExceptionHandler();
        if (null != flowExceptionHandler) {
            exceptionHandlers.add(flowExceptionHandler);
        }

        if ((getNode() instanceof JavaMethodTask)) {
            JavaMethodTask javaMethodTask = (JavaMethodTask) getNode();
            ExceptionHandler taskExceptionHandler = javaMethodTask.getExceptionHandler();
            if (null != taskExceptionHandler) {
                exceptionHandlers.add(taskExceptionHandler);
            }
        }

        List<MethodInfo> stopCheckMethod = new ArrayList<>();
        for (ExceptionHandler exceptionHandler : exceptionHandlers) {
            Type exceptionHandlerType = exceptionHandler.getType();
            FormattedMethodString method = exceptionHandler.getMethod();
            if (method == null || exceptionHandlerType == null) {
                continue;
            }
            String exceptionHandlerMethod = method.getName();
            ClassInfo aClass = getProject().find(exceptionHandlerType.getQualifiedName());
            addImport(exceptionHandlerType);
            MethodInfo[] methods = aClass.getMethods();
            Optional<MethodInfo> optional = Arrays.stream(methods)
                    .filter(m -> m.getName().equals(exceptionHandlerMethod))
                    .filter(m -> m.getParameterCount() == 1)
                    .filter(m -> m.getParameterTypes()[0].isAssignableTo(Exception.class))
                    .findFirst();
            optional.ifPresent(stopCheckMethod::add);
        }

        stopCheckMethod.sort((m1, m2) -> {
            if (m1.getParameterTypes()[0].isAssignableTo(m2.getParameterTypes()[0])) {
                return -1;
            }
            if (m2.getParameterTypes()[0].isAssignableTo(m1.getParameterTypes()[0])) {
                return 1;
            }
            return 0;
        });
        return stopCheckMethod;
    }


    /**
     * Write flow output.
     *
     * @param autoTaskBody the auto task body
     */
    protected void writeFlowOutput(BlockStmt autoTaskBody) {
        Output flowOutput = flowNodeSource.getOutput();
        if (flowOutput == null) {
            return;
        }

        for (Return aReturn : flowNodeSource.getReturns()) {
            for (ParamValue value : aReturn.getValues()) {
                if (value.getSource().equals(node.getName())) {
                    if (flowOutput.isSimpleType()) {
                        autoTaskBody.addStatement(format("context.set%sOutput(%s);",
                                flowNodeSource.getSimpleName(),
                                processValueSource(value, flowNodeSource)));
                    } else {
                        String[] fieldNames = aReturn.getFieldNames();
                        if (fieldNames.length == 1) {
                            autoTaskBody.addStatement(format("context.set%sOutput(%s);",
                                    flowNodeSource.getSimpleName(),
                                    processValueSource(value, flowNodeSource)));
                        } else {
                            autoTaskBody.addStatement(format("context.get%sOutput().set%s(%s);",
                                    flowNodeSource.getSimpleName(),
                                    capitalize(fieldNames[1]),
                                    processValueSource(value, flowNodeSource)));
                        }
                    }
                }
            }
        }
    }

    /**
     * Handler successor.
     *
     * @param successor the successor
     * @param body      the body
     */
    protected void handlerSuccessor(Node successor, NodeWithStatements<?> body) {
        if (null != successor) {
            body.addStatement(format("%sStep %s = new %sStep();", successor.getCapitalizeName(),
                    successor.getUnCapitalizeName(),
                    successor.getCapitalizeName()));

            BlockStmt tryBody = new BlockStmt();
            tryBody.addStatement(format("%s.call(context, flow);", successor.getUnCapitalizeName()));

            BlockStmt catchBody = new BlockStmt();
            catchBody.addStatement(String.format("%s.error(context, flow, e);", successor.getUnCapitalizeName()));

            ClassOrInterfaceType exceptionType = new ClassOrInterfaceType("java.lang.Exception");
            addImport(SimpleType.of("java.lang.Exception"));
            CatchClause catchClause = new CatchClause(new Parameter(exceptionType, "e"), catchBody);

            body.addStatement(new TryStmt(tryBody, NodeList.nodeList(catchClause), null));
        }
    }

    /**
     * Process value source string.
     *
     * @param paramValue the param value
     * @param flowNode   the flow node
     * @return the string
     */
    protected String processValueSource(ParamValue paramValue, FlowJavaSource flowNode) {
        StringBuilder ex = new StringBuilder("NullSafe.get(()->context.get");
        String source = paramValue.getSource();
        int i = 0;
        final String[] fields = paramValue.getFields();
        if (fields == null || fields.length == 0) {
            return "";
        }
        if (flowNode.getFlowName().equals(source)) {
            ex.append(flowNode.getInputClassName());
        } else {
            ex.append(capitalize(source));
            if (fields[0].equals("output")) {
                ex.append("Output");
                i++;
            }
        }

        ex.append("()");

        for (; i < fields.length; i++) {
            ex.append(".get").append(capitalize(fields[i])).append("()");
        }
        ex.append(")");
        return ex.toString();
    }

    /**
     * Process input imports.
     *
     * @param switchTaskBody the switch task body
     * @param inputs         the inputs
     * @param flowNode       the flow node
     * @return the names of variable initialed by context
     */
    protected List<String> prepareStepParams(BlockStmt switchTaskBody, Collection<Param> inputs, FlowJavaSource flowNode) {
        List<String> varNames = new ArrayList<>();
        for (Param input : inputs) {
            //TODO shunyun 2024/1/17: defaultValue
            ParamValue value = input.getValue();
            if (null == value) {
                continue;
            }
            String valueExpr = processValueSource(value, flowNode);
            if (StringUtils.isNotBlank(valueExpr)) {
                addImport(input.getType());
                switchTaskBody.addStatement(format("%s %s = %s;",
                        input.getType().getName(),
                        input.getName(),
                        valueExpr
                ));
                varNames.add(input.getName());
            }
        }
        return varNames;
    }

}
